#! /usr/bin/env python

# -*- coding: UTF-8 -*-
# ==============================================================================
# Filename : TextEdit.py
# Purpose  :
# Author   : Yohann Li (lyh001686@sina.com)
# Date     : 2020-07-01 17:10
# ==============================================================================
import os
import string
import sys
from collections import deque
from functools import partial

from PyQt5.QtCore import pyqtSignal, QTimer, QObject, QEvent, QThread, QRegularExpression, Qt, QFile, QTextStream, \
    QStringListModel
from PyQt5.QtGui import QDragEnterEvent, QDropEvent, QTextCursor, QKeySequence
from PyQt5.QtWidgets import QApplication, QPlainTextEdit, QFileDialog, QMessageBox, QCompleter

from Widgets.Highlighter2 import Highlighter
from Widgets.ScrollBar import ScrollBar
# from Widgets.SshEdit import SSHEdit
from conf.configuration import logger, mode_mark, valid_cmd_pat, OS_Prefix, APPNAME, Key_Delay, re_cur_color_bac
from conf.cmds import AutoCompleteCommands


class TextEdit(QPlainTextEdit):
    open_pySignal = pyqtSignal(str)  # 发送文件路径
    n_match_pySignal = pyqtSignal(int)  # 发送匹配文本数量到search bar
    finding_state_pySignal = pyqtSignal(bool)
    key_pressed_signal = pyqtSignal(str)
    tab_pressed_signal = pyqtSignal(str)
    fun_pressed_signal = pyqtSignal(str)  # key pressed {Backspace, Left/Right Arrow}
    pass_key_event_signal = pyqtSignal(QEvent)  # 同步keyPressEvent到Minimap
    title_pySignal = pyqtSignal(str)  # 设置title

    def __init__(self, name: str = '', port_offset: int = 1000, *args, **kwargs):
        super(TextEdit, self).__init__()
        self.setLineWrapMode(QPlainTextEdit.NoWrap)
        self.setObjectName(name + '_edit')
        logger.debug(f"OBName: {self.objectName()}")
        self.setTabStopWidth(4)
        self.setTabChangesFocus(False)
        self.setAcceptDrops(True)
        self.setAutoFillBackground(True)
        self.setProperty('find_state', 0)
        self.setAttribute(Qt.WA_InputMethodEnabled, False)  # 禁止使用输入法
        self.setCursorWidth(5)  # 设置光标外形宽度
        self.setContentsMargins(8, 0, 8, 10)
        self._file_path = ''
        self.file_name = ''
        self._cursor_pre_pos = None
        self._serial_opened = False
        self.mode = ''  # 模式
        self._diags_prefix = ''  # diags下的命令行前缀
        self.cmd_history = {'OS': deque(maxlen=100),
                            'Diags': deque(maxlen=100),
                            'Recovery': deque(maxlen=100)}  # 用来存储当前模式下的cmd历史记录
        self.prefix = {'OS': '~ root# ',
                       'Diags': self.diags_prefix,
                       'Recovery': '] '}
        self._tracker = 0
        self._cmd_long = ''  # 存储当前输入命令
        self._prev_cmd = ''  # 存储上一条stdin
        self._rmt_stdin = ''  # 存储远端待运行待stdin
        self._tab_status = 0  # 看是否按下tab键
        self._bac_status = 0  # 看是否按下backspace键
        self._del_status = 0  # del 键
        self._lef_status = 0  # left  arrow status
        self._rig_status = 0  # right arrow status
        self._return_status = 0  # 看是否按下return键

        self._cur_pos = 0

        self.sBar = ScrollBar(self)
        self.setVerticalScrollBar(self.sBar)
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.highlighter = Highlighter(self.document())
        self.read_timer = QTimer(self)
        # ---- key timer ---
        self.right_arr_timer = QTimer(self)
        self.left_arr_timer = QTimer(self)
        self.return_timer = QTimer(self)
        self.paste_timer = QTimer(self)  # 粘贴timer，用来在粘贴之后将cursor重置到END
        self.bac_timer = QTimer(self)
        self.tab_timer = QTimer(self)
        self.del_timer = QTimer(self)

        self.right_arr_timer.timeout.connect(partial(self.reset_key_status, key="right_arr"))
        self.left_arr_timer.timeout.connect(partial(self.reset_key_status, key="left_arr"))
        self.return_timer.timeout.connect(partial(self.reset_key_status, key="return"))
        self.bac_timer.timeout.connect(partial(self.reset_key_status, key="bac"))
        self.tab_timer.timeout.connect(partial(self.reset_key_status, key="tab"))
        self.del_timer.timeout.connect(partial(self.reset_key_status, key="del"))
        self.key_timer_map = {"tab": {"timer": self.tab_timer, "status": "tab_status"},
                              "del": {"timer": self.del_timer, "status": "del_status"},
                              "bac": {"timer": self.bac_timer, "status": "bac_status"},
                              "return": {"timer": self.return_timer, "status": "return_status"},
                              "right_arr": {"timer": self.right_arr_timer,"status": "right_arr_status"},
                              "left_arr": {"timer": self.left_arr_timer,"status": "left_arr_status"},}

        self.read_delay = 100
        self.line_cont = ''  # 存储读取到的内容
        # self.read_timer.start(self.read_delay)  # this timer is for ssh edit
        self.read_timer.timeout.connect(self.read_timer_timeout)
        self.paste_timer.timeout.connect(self.paste_timer_timeout)
        self.finding_state_pySignal.connect(self.finding_state_changed)

        self._completer = QCompleter(self)
        self._completer.setWidget(self)
        self._completer.setModel(QStringListModel(self.auto_complete_commands, self._completer))
        self._completion_prefix = ''
        self._completer.setCompletionMode(QCompleter.PopupCompletion)
        # self._completer.setCaseSensitivity(Qt.CaseInsensitive)
        self._completer.activated.connect(self._insert_completion)

        self.process_event_timer = QTimer(self)
        self.process_event_timer.timeout.connect(self.process_events)
        self.restart_process_events_timer()

        if kwargs.get('event'):
            logger.debug(f"installing Event Filter")
            self.installEventFilter(self)
        self.key_pressed_signal.connect(self.append_cmd_history)
        # self.tab_pressed_signal.connect(partial(self.pressed_key, key='tab'))
        # self.cursorPositionChanged.connect()

    def restart_process_events_timer(self):
        # logger.debug('Restarting process events timer for 1 second')
        if self.process_event_timer.isActive():
            self.process_event_timer.stop()
        self.process_event_timer.start(1000)

    def process_events(self):
        QApplication.processEvents()
        self.restart_process_events_timer()

    def mode_switched(self, org_mode='', new_mode=''):
        """当切换了模式的时候要更换completer中的model"""
        logger.debug(f"Mode switced from {org_mode} to {new_mode}")
        self._completer.setModel(QStringListModel(self.auto_complete_commands, self._completer))

    def no_need_echo_status(self):
        """按下 backspace/delete/left/right 任意键的情况下阻止接收任何数据"""
        # print(self.bac_status, self.del_status, self.left_arr_status, self.right_arr_status)
        return (self.bac_status or self.del_status or self.left_arr_status or self.right_arr_status)

    def resume_echo_status(self):
        """该函数将在按下printable key的时候调用，防止在no echo timer结束前按下字母数字却收不到数据"""
        self.bac_status = self.del_status = self.left_arr_status = self.right_arr_status = 0

    def restart_read_timer(self):
        """ssh 0.1秒未收到数据，就把缓存发出来"""
        if self.read_timer.isActive():
            self.read_timer.stop()
        self.read_timer.start(self.read_delay)

    def recv_(self, r: str):
        """ssh"""
        self.line_cont += r if not self.no_need_echo_status() else ''
        self.restart_read_timer()
        if not self.line_cont.__contains__('\n'):  # 等待换行符
            return
        *_r, self.line_cont = self.line_cont.splitlines(True)
        for __r in _r:
            __r = re_cur_color_bac.sub('', __r)
            logger.debug(repr(__r))
            if __r == '\n' and not self.return_status:  # 阻止提示超时产生的\n  ?? 还有这回事儿？
                return
            self.insert_at_end(__r)
            if self.tab_status:
                self.cmd_long = ''

    def read_timer_timeout(self):
        # self.read_timer.stop()
        """ssh读取超时,处理未成行的数据
        tab键的吐出一般都在这里面"""
        if self.line_cont and not self.no_need_echo_status():
            __r, self.line_cont = self.line_cont, ''
            __r = re_cur_color_bac.sub('', __r)
            logger.debug(repr(__r))
            __r = self.deal_by_status(__r)

            # self.insert_at_end(__r)
            if __r:
                self.insert(__r)
                self.estimate_len_of_os_prefix(__r)
        elif self.bac_status and self.cmd_long and self.textCursor().atBlockEnd():  # 没有line cont, 有cmd_long, 在行末, 按backspace
            logger.debug(f"deleting the last one letter")
            # self.rmt_cmd = self.rmt_cmd[:-1]

    def append_cmd_history(self, cmd: str):
        """添加命令记录"""
        logger.debug(f"\033[35m{repr(cmd)}\033[0m")
        _cmd = self.rmt_cmd.strip() + cmd.strip()
        # 如有符合以下条件，则添加到命令记录
        # 1. 有确认模式在OS,iBoot,Diags
        # 2. 有英文输入或远端有命令存在
        # 3. 按下了return键
        if self.mode and (valid_cmd_pat.search(cmd) or self.rmt_cmd) and self.return_status:
            self.cmd_history.get(self.mode).append(_cmd)
            logger.debug(
                f"\033[33m{self.mode} mode append cmd history: {repr(self.rmt_cmd.strip() + cmd.strip())}\033[0m")

    def cur_pos_in_cmd(self) -> int:
        """当前光标在命令中的位置"""
        # 未知模式 return 0
        if not self.prefix.get(self.mode):
            return 0
        prefix = self.prefix.get(self.mode)
        prefix_len = len(prefix)
        pos_in_cmd = self.textCursor().positionInBlock() - prefix_len
        # 当前行前缀是模式前缀的时候返回光标在命令中的位置
        if self.textCursor().block().text().__contains__(prefix):
            return pos_in_cmd
        # 返回光标在行中的位置
        else:
            logger.debug(f"Current block don't have {repr(self.mode)} prefix.")
            return self.textCursor().positionInBlock()
        # logger.debug(
        # f"cursor postion: {prefix_len}+{pos}={self.textCursor().positionInBlock()} {self.prefix.get(self.mode)}")
        # logger.debug(f"Overall cursor position: {self.textCursor().position()}")

    def deal_by_status(self, __r):
        _cur_pos = self.cur_pos_in_cmd()
        if self.tab_status:  # 1. L\t -> L : 2. Lib\t -> Library/  3. \t -> ***
            # if self.cmd_long and __r.startswith(self.cmd_long):
            self.rmt_cmd += __r
            # self.pre_cmd = __r
            self.cmd_long += __r
        # elif self.bac_status:  # TODO 基本无用，待删除
        #     logger.debug(
        #         f"Pressed backspace, __r {repr(__r)} {repr(self.rmt_cmd)}[{_cur_pos}:] {repr(self.rmt_cmd[_cur_pos:])}")
        #     # if self.rmt_cmd and __r == self.rmt_cmd[_cur_pos:]:  # rmt cmd 有值 并且读到数据等于光标后的字符，则清空读到的数据
        #     __r = ''  # comment above: 甭管遇到啥，消除字符都时候啥都不读
        # elif self.del_status:  # TODO 基本无用，待删除
        #     logger.debug(
        #         f"Pressed delete, __r {repr(__r)} self.rmt_cmd[{self.cur_pos}:] {repr(self.rmt_cmd[self.cur_pos:])}")
        #     if self.rmt_cmd and __r == self.rmt_cmd[self._cur_pos:]:
        #         __r = ''
        # elif self.left_arr_status or self.right_arr_status:  # TODO 基本无用，待删除
        #     logger.debug(
        #         f"Pressed arrow, __r {repr(__r)} self.rmt_cmd[{self.cur_pos}:] {repr(self.rmt_cmd[self.cur_pos:])}")
        #     # if self.rmt_cmd and __r == self.rmt_cmd[self._cur_pos:]:
        #     __r = ''  # 左右移动光标没有理由接收数据
        elif not self.textCursor().atBlockEnd():  # 光标不在行末
            logger.debug(
                f"Cursor in block, __r {repr(__r)} {repr(self.rmt_cmd)}[{_cur_pos}:] {repr(self.rmt_cmd[_cur_pos:])}")
            # if __r:  # 把光标后的字符全部删掉然后再把读到的数据沾上去，省的不停的判断
            #     self.moveCursor(QTextCursor.End, QTextCursor.KeepAnchor)
            #     self.textCursor().removeSelectedText()
            #     return __r
            # deprecated: 每次这样操作完后，local光标会自动移动到行末，但是remote光标不会移动到行末。
            # 在这个函数里又无法移回去，在别的函数移回去又麻烦！恶心！还是用判断的方法吧
            _block = self.textCursor().block().text()
            _cur_pos_ = self.textCursor().positionInBlock()
            _forward_str = _block[_cur_pos_:]  # 从光标位置到行末的字符串
            logger.debug(f"forward string: {_forward_str}")
            if self.rmt_cmd and __r == self.rmt_cmd[_cur_pos:] and __r != _forward_str:
                __r = __r[0]
                logger.debug(f"")
            elif self.rmt_cmd and __r == self.rmt_cmd[_cur_pos:]:
                __r = ''
                logger.debug(f"")
            else:
                logger.debug(f"{repr(__r)} == {repr(self.rmt_cmd[_cur_pos:])} {bool(__r == self.rmt_cmd[_cur_pos:])}")
        return __r

    def insert_at_end(self, text: str):
        logger.debug(f"\033[33m{repr(text)}\033[0m")
        self.moveCursor(QTextCursor.End, QTextCursor.MoveAnchor)
        self.insertPlainText(text)
        self.sBar.scroll_to_bottom()
        # QApplication.processEvents(QEventLoop.ExcludeUserInputEvents)

    def insert(self, text: str):
        # logger.debug(f"\033[33m{repr(text)}\033[0m")
        self.insertPlainText(text) if text != '|test|' else ''
        self.sBar.scroll_to_bottom()
        # QApplication.processEvents()
        # if self.paste_timer.isActive():

    def _rmt_cmd_insert(self, index: int, v: str):
        """更改rmt_cmd, 在index位置插入cmd"""
        _f = self.rmt_cmd[:index]
        _b = self.rmt_cmd[index:]
        logger.debug(f"insert v at position:{index} -> {repr(_f)},{repr(v)},{repr(_b)}")
        self.rmt_cmd = ''.join((_f, v, _b))

    def estimate_len_of_os_prefix(self, r: str):
        if OS_Prefix.search(r):
            pre_prefix = self.prefix.get('OS')
            cur_prefix = OS_Prefix.match(r).group('osp')
            self.prefix.__setitem__('OS', cur_prefix)
            logger.debug(f"Change cwd from {repr(pre_prefix)} to {repr(cur_prefix)}")
        return

    def _insert_completion(self, c: str):
        logger.debug(f"completion: {repr(c)}, prefix{repr(self.completion_prefix)}")
        extra = len(c) - len(self.completion_prefix)
        if not extra: return
        fill_text = c[-extra:]
        self.reset_cursor_pos()
        self.key_pressed_signal.emit(fill_text)
        self.rmt_cmd += fill_text
        self.cmd_long += fill_text

    def word_under_cursor(self, c):  # 应该不起作用了，completer获取prefix的方式改为了读取cmd_long
        tc = self.textCursor()
        tc.select(QTextCursor.WordUnderCursor)
        print('Here is the selected text under the cursor', tc.selectedText())
        return tc.selectedText()+c

    @property
    def auto_complete_commands(self):
        return AutoCompleteCommands.get(self.mode or 'OS', [])

    @property
    def completion_prefix(self):
        return self._completion_prefix

    @completion_prefix.setter
    def completion_prefix(self, v: str):
        logger.debug(f"Completer prefix revise from {repr(self._completion_prefix)} to {repr(v)}")
        self._completion_prefix = v

    @property
    def file_path(self):
        return self._file_path

    @file_path.setter
    def file_path(self, v: str):
        logger.debug(f"File path revise from {repr(self._file_path)} to {repr(v)}")
        self._file_path = v
        self.file_name = os.path.basename(v)

    @property
    def cur_pos(self):
        return self._cur_pos

    @cur_pos.setter
    def cur_pos(self, v: int):
        logger.debug(f"cursor moved {v} bit")
        self._cur_pos = v

    @property
    def cursor_pre_pos(self):
        return self._cursor_pre_pos

    @cursor_pre_pos.setter
    def cursor_pre_pos(self, p):
        logger.debug(f"Setting cursor pre postion: {p}")
        self._cursor_pre_pos = p

    @property
    def pre_cmd(self):
        return self._prev_cmd

    @pre_cmd.setter
    def pre_cmd(self, v: str):
        self._prev_cmd = v

    @property
    def cmd_long(self):
        return self._cmd_long

    @cmd_long.setter
    def cmd_long(self, cmd: str):
        self.pre_cmd = self._cmd_long
        logger.debug(f"Revise local  cmd {repr(self.pre_cmd)} to {repr(cmd)}")
        self._cmd_long = cmd

    @property
    def rmt_cmd(self):
        return self._rmt_stdin

    @rmt_cmd.setter
    def rmt_cmd(self, v: str):
        logger.debug(f"Revise remote cmd {repr(self.rmt_cmd)} to {repr(v)}")
        self._rmt_stdin = v

    @property
    def diags_prefix(self):
        return self._diags_prefix

    @diags_prefix.setter
    def diags_prefix(self, prefix: str):
        self._diags_prefix = prefix
        self.prefix.__setitem__('Diags', prefix)

    @property
    def serial_opened(self):
        return self._serial_opened

    @serial_opened.setter
    def serial_opened(self, v: bool):
        logger.debug(f"Revise serial open status {self.serial_opened} to {v}")
        self._serial_opened = v

    @property
    def tab_status(self):
        return self._tab_status

    @tab_status.setter
    def tab_status(self, v):
        self._tab_status = v

    @property
    def del_status(self):
        return self._del_status

    @del_status.setter
    def del_status(self, v):
        self._del_status = v

    @property
    def bac_status(self):
        return self._bac_status

    @bac_status.setter
    def bac_status(self, v):
        self._bac_status = v

    @property
    def return_status(self):
        return self._return_status

    @return_status.setter
    def return_status(self, v):
        self._return_status = v

    @property
    def right_arr_status(self):
        return self._rig_status

    @right_arr_status.setter
    def right_arr_status(self, v):
        self._rig_status = v

    @property
    def left_arr_status(self):
        return self._lef_status

    @left_arr_status.setter
    def left_arr_status(self, v):
        self._lef_status = v

    def pressed_key(self, key: str = ''):
        """key enum:
            1. tab
            2. del
            3. bac
            4. return
            5. right_arr
            6. left_arr
        """
        logger.debug(f" ---------> Pressed {key} <----------")
        timer_dict = self.key_timer_map.get(key)
        assert timer_dict, f"{key} timer is {timer_dict}"
        timer = timer_dict.get('timer')
        if timer.isActive():
            timer.stop()
        self.__setattr__(timer_dict.get('status'), 1)
        timer.start(Key_Delay)
        logger.debug(f"Set {timer_dict.get('status')} to {self.__getattribute__(timer_dict.get('status'))}")

    def reset_key_status(self, key: str = ''):
        timer_dict = self.key_timer_map.get(key)
        assert timer_dict, f"{key} timer is {timer_dict}"
        timer = timer_dict.get('timer')
        timer.stop()
        self.__setattr__(timer_dict.get('status'), 0)
        logger.debug(f"Reset {timer_dict.get('status')} to {self.__getattribute__(timer_dict.get('status'))}")

    def paste_timer_timeout(self):
        if self.paste_timer.isActive():
            self.paste_timer.stop()
            self.cursor_pre_pos = self.textCursor().position()
            logger.debug(f"Reset cursor position to {self.cursor_pre_pos}")

    def del_cur_line(self):
        cursor = self.textCursor()
        cursor.select(QTextCursor.BlockUnderCursor)
        cursor.removeSelectedText()

    def reset_cur_line(self):
        """删除远端命令，删除本地当前行，并重写当前行的前缀"""
        self.del_cur_line()
        self.appendPlainText(self.prefix.get(self.mode, ''))

    def reset_cursor_pos_to_end(self):
        logger.debug("Move position to the end")
        self.textCursor().movePosition(QTextCursor.End, QTextCursor.MoveAnchor)

    def mousePressEvent(self, e) -> None:
        if not isinstance(self.cursor_pre_pos, int):
            self.cursor_pre_pos = self.textCursor().position()
            logger.debug(f"Mouse clicked, setting cursor pre position {repr(self.cursor_pre_pos)}")
        logger.info(f"block count: {self.blockCount()}")
        super(TextEdit, self).mousePressEvent(e)

    def mouseDoubleClickEvent(self, e) -> None:
        return
        # return super(TextEdit, self).mousePressEvent(e)

    def reset_cursor_pos(self):
        """鼠标点击会改变光标位置, 记住光标原位置，输入的时候光标回到原位置"""
        if isinstance(self.cursor_pre_pos, int):
            logger.debug(f"Resetting cursor to position {self.cursor_pre_pos}")
            cur = self.textCursor()
            cur.setPosition(self.cursor_pre_pos)
            self.setTextCursor(cur)
            self.cursor_pre_pos = None

    def del_previous_char(self, n: int = 1):
        for _ in range(n):
            self.textCursor().deletePreviousChar()
        logger.debug(f"Deleted previous {n} char")

    def finding_state_changed(self, v: bool):
        """slot"""
        self.setProperty('find_state', 1 if v else 0)
        self.style().polish(self)

    def get_prefix(self):
        """get prefix in line"""
        text = self.textCursor().block().text()
        try:
            prefix = mode_mark.match(text).group(1)
            return prefix
        except AttributeError as e:
            logger.error(f"{mode_mark.pattern} match {text}")
            return ""

    def find_text(self, find: dict):
        """
        1. 查找内容 2.
        :param find: {'exp': 'str or re', 'options': QTextDocument::FindFlags, 'direction': 'up or down'}
        :return:
        """
        exp = find.get('exp')
        options = find.get('options')
        # logger.debug(f'Finding {exp} with options {options}...')
        self.count_thread = CountThread(exp, self.toPlainText(), word=find.get('word'), case=find.get('case'))
        self.count_thread.count_signal.connect(self.set_n_matches)
        self.count_thread.start()

        r = self.find(exp, options) if options else self.find(exp)
        previous_postion = self.textCursor().position()
        if not r:  # 没有搜索到内容
            # 如果是向下搜索，则将光标移到开始位置，否则移到最后
            self.moveCursor(QTextCursor.Start, QTextCursor.MoveAnchor) \
                if find.get('direction') == 'down' else \
                self.moveCursor(QTextCursor.End, QTextCursor.MoveAnchor)
            r = self.find(exp, options) if options else self.find(exp)

        if r:
            logger.debug(f"Found {repr(self.textCursor().selectedText())}")
        else:
            cur = self.textCursor()
            cur.setPosition(previous_postion, QTextCursor.MoveAnchor)
            self.setTextCursor(cur)

    def dragEnterEvent(self, a0: QDragEnterEvent) -> None:
        if a0.mimeData().hasUrls():
            a0.accept()
        else:
            a0.ignore()

    def dropEvent(self, a0: QDropEvent) -> None:
        logger.debug(f'Source: {a0.source()}')
        if a0.mimeData().hasText():
            self.key_pressed_signal.emit(a0.mimeData().text())
            a0.accept()
        elif a0.mimeData().hasUrls():
            file_path = a0.mimeData().urls()[0].toLocalFile()
            logger.debug(file_path)
            self.open_pySignal.emit(file_path)
            a0.accept()
        else:
            a0.ignore()

    def set_n_matches(self, count: int):
        """receive count from thread and emit to search bar widget"""
        self.n_match_pySignal.emit(count)

    def search_bar_hide_event(self):
        logger.debug('Search bar hidden')
        # self.highlighter = Highlighter(self.document())
        self.highlighter.find_text = ''

    def save(self):
        if self.file_path:
            self.write_file(self.file_path)
        else:
            logger.warning(f"Local path {repr(self.file_path)} don't exist, switch to save as...")
            self.save_as()

    def save_as(self):
        fp, _ = QFileDialog.getSaveFileName(self, 'Save file to', os.path.expanduser('~/Desktop'), filter='Any(*);;')
        if not fp:
            return
        self.file_path = fp
        logger.debug(f"fp: {fp}")
        self.write_file(fp)

    def write_file(self, fp: str):
        logger.debug(f"Write to file, path = {repr(fp)}")
        file = QFile(fp)
        if not file.open(QFile.WriteOnly | QFile.Text):
            QMessageBox.warning(self, APPNAME, f"Cannot write file {fp}")
            return False
        cont = QTextStream(file)
        QApplication.setOverrideCursor(Qt.WaitCursor)
        cont << self.toPlainText()
        QApplication.restoreOverrideCursor()

    def hide_completer(self, v):
        logger.debug(f"Hidding completer {v}") if v else ''
        self._completer.popup().hide()

    # def mousePressEvent(self, e) -> None:
    #     logger.debug(f"printed {self.objectName()}")
    #     return super(TextEdit, self).mousePressEvent(e)

    # def focusOutEvent(self, e) -> None:
    #     self.setFocus()

    def eventFilter(self, a0: QObject, a1: QEvent) -> bool:
        if a1.type() == QEvent.KeyPress:
            """return事件不传递到编辑器"""
            # self.pass_key_event_signal.emit(a1)  # 发送信号给Minimap的keyPressEvent
            key = a1.text()
            if a1.key() == Qt.Key_Tab:  # tab
                """1. 远端没有补充cmd,且无输出
                    ~ root# L\t -> ~ root# L

                2. 远端没有补充cmd,但有cmd选项输出(需要按下两次tab)
                    ~ root# L\t\t -> 
                    ~ root# L
LCDTest                          LaunchMarathon                   ListLoad
                         ListLoadCmd                    ListLoadCmd_DTrace-Automated.sh
                3. 远端补充了部分cmd
                4. 远端补全了cmd
                    ~ root# Lib\t -> ~ root# Library"""
                logger.debug(f"cmd long: {self.cmd_long}")
                key = '\t'
                # for _ in range(len(self.cmd_long)):
                #     self.textCursor().deletePreviousChar()
                # self.key_pressed_signal.emit(key)
                # self.tab_pressed_signal.emit(self.cmd_long + key)  # printable key 储存等待\n或\t发送的情况
                # 例如: rmt = local = 'Library', 然后将cursor移到行首，输入ls， 再将光标移到行末，
                # 此时按下return键，rmt = 'Library ls\n', local = 'ls Library\n'
                # 遂此种方法不可取
                # ______________________________________________________
                self.tab_pressed_signal.emit(key)  # printable key 随时发送的情况
                self.pressed_key(key='tab')
                return True
            elif a1.key() == Qt.Key_Up:  # arrow up  ^[[A
                if self.cmd_history.get(self.mode, ''):
                    try:
                        self._tracker -= 1
                        last_cmd = self.cmd_history.get(self.mode)[self._tracker]
                    except IndexError:
                        self._tracker = -1
                        last_cmd = self.cmd_history.get(self.mode)[self._tracker]
                    self.fun_pressed_signal.emit('\x15') if self.rmt_cmd else ''  # 如果此时远端有stdin,则删除
                    self.cmd_long = self.rmt_cmd = last_cmd
                    # 重写当前行的前缀
                    self.reset_cursor_pos()
                    self.reset_cur_line()
                    # 插入上一条命令
                    # self.insert(last_cmd)
                    self.key_pressed_signal.emit(self.cmd_long)
                return True
            elif a1.key() == Qt.Key_Down:  # arrow down  ^[[B
                if self.cmd_history.get(self.mode, ''):
                    try:
                        self._tracker += 1
                        last_cmd = self.cmd_history.get(self.mode)[self._tracker]
                    except IndexError:
                        self._tracker = 0
                        last_cmd = self.cmd_history.get(self.mode)[self._tracker]
                    self.fun_pressed_signal.emit('\x15') if self.rmt_cmd else ''  # 如果此时远端有stdin,则删除
                    self.cmd_long = self.rmt_cmd = last_cmd
                    self.reset_cursor_pos()
                    self.reset_cur_line()
                    # self.insert(last_cmd)
                    self.key_pressed_signal.emit(self.cmd_long)
                return True
            elif a1.key() == Qt.Key_Left:  # arrow left  ^[[D
                self.pressed_key(key="left_arr")
                pos_in_block = self.textCursor().positionInBlock  # 获取光标位置
                prefix_len = len(self.prefix.get(self.mode))
                logger.debug(f"prefix: {repr(self.prefix.get(self.mode))} "
                             f"prefix_len: {prefix_len} pos_in_block: {pos_in_block()}")
                if prefix_len >= pos_in_block() and \
                        self.textCursor().block().text().__contains__(self.prefix.get(self.mode, '')):
                    return True
                self.fun_pressed_signal.emit('\033[D')
                if a1.modifiers() & Qt.AltModifier:
                    logger.debug(f"Alt + Left disabled")
                    return True
                    # cur = self.textCursor()
                    # cur.movePosition(QTextCursor.PreviousWord, QTextCursor.MoveAnchor)
                    # self.setTextCursor(cur)
                    # # self.fun_pressed_signal.emit("\x12\x1b[D")
                    # self.fun_pressed_signal.emit("\x1b[1;3D")
                self.cur_pos = pos_in_block() - prefix_len  # 这个当前位置是光标移动前的位置
                # self.cur_pos = pos_in_block()  # 这个当前位置是光标移动前的位置
                logger.debug(f"cur cursor pos: {self.cur_pos}")
                # return True
            elif a1.key() == Qt.Key_Right:  # arrow right  ^[[C
                self.pressed_key(key='right_arr')
                self.fun_pressed_signal.emit('\x1b[C')
                self.cur_pos += 1
                if a1.modifiers() & Qt.AltModifier:
                    logger.debug(f"Alt + Right disabled")
                    return True
            elif a1.matches(QKeySequence.Paste):
                logger.debug(f" ---------> Pressed Paste <----------")
                key = QApplication.clipboard().text()
                forward_cmd = ''
                if self.textCursor().atEnd():
                    logger.debug("Textcursor at end.")
                    self.rmt_cmd += key
                    self.cmd_long += key
                else:
                    cur_pos = self.cur_pos_in_cmd()
                    forward_cmd = self.rmt_cmd[cur_pos:]
                    self.rmt_cmd = self.rmt_cmd[:cur_pos] + key + self.rmt_cmd[cur_pos:]
                    self.cmd_long = self.cmd_long[:cur_pos] + key + self.cmd_long[cur_pos:]
                self.reset_cursor_pos()
                # 移除edit光标后到字符
                self.key_pressed_signal.emit("\x0B")
                self.moveCursor(QTextCursor.End, QTextCursor.KeepAnchor)
                self.textCursor().removeSelectedText()

                self.key_pressed_signal.emit(f"{key}{forward_cmd}")
                self.paste_timer.start(self.read_delay * 2)
                # 粘贴后再把edit的光标移动到原位置
                # [self.moveCursor(QTextCursor.Left, QTextCursor.MoveAnchor) for _ in range(len(forward_cmd))]

                return True
            # elif a1.key() == Qt.Key_Control:
            #     logger.debug(f"pressed ctrl")
            #     return True
            # elif a1.key() == Qt.Key_Meta:
            #     logger.debug(f"pressed meta")

            elif a1.key() == Qt.Key_Escape:
                logger.debug(f"Pressed Esc")
                self.fun_pressed_signal.emit('\x1B')
        return super(TextEdit, self).eventFilter(a0, a1)

    def keyPressEvent(self, e) -> None:
        # if e.text() in string.printable:
        #     self.pass_key_event_signal.emit(e.text())
        #     return
        # elif e.key() == Qt.Key_Tab
        ctrlOrShift = e.modifiers() & (Qt.ControlModifier | Qt.ShiftModifier)
        hasModifier = (e.modifiers() != Qt.NoModifier) and not ctrlOrShift  # 非Ctrl/Shift的控制键
        # 按下非Ctrl/Shift/Printable Key，不显示completer
        # logger.debug(f"key not shift, meta {e.modifiers() not in (Qt.ShiftModifier, Qt.MetaModifier)}")
        logger.debug(f"hasModifier: {repr(hasModifier)}, key: {repr(e.text())}, "
                     f"len: {repr(len(e.text()))}, keyID: {repr(e.key())}")
        if (hasModifier or len(e.text()) == 0) and e.modifiers() not in (Qt.ShiftModifier, Qt.MetaModifier):
            # if (hasModifier or len(e.text())==0) and e.key() > 250:
            logger.debug(f"e.text len = 0; {repr(e.text())}")
            self.hide_completer(744)
            return super(TextEdit, self).keyPressEvent(e)  # 这个return会阻挡快捷键，例如Ctrl+A, 去掉条件 len(e.text()) == 0 后暂时可以用
            # return
        # Completer可视情况下，按Tab 选择item
        if self._completer.popup().isVisible() and e.key() in (Qt.Key_Tab,):  # 取消使用回车键选择item的功能
            # logger.debug(f"")
            e.ignore()
            return

        key = e.text()

        if e.text() and e.text() in string.digits + string.ascii_letters + string.punctuation + ' ':
            logger.debug(f"Pressed key {repr(key)} {e.key()}")
            self.resume_echo_status()
            self._tracker = 0  # reset tracker to 0
            self.reset_cursor_pos()

            if self.cmd_long and self.cmd_long.startswith(self.rmt_cmd) and self.rmt_cmd != self.cmd_long:
                # 如果local为真且local真包含rmt
                _index = self.cmd_long.index(self.rmt_cmd) + len(self.rmt_cmd)
                _key = self.cmd_long[_index:] + key
                self.reset_cur_line()
                self.key_pressed_signal.emit(_key)
                logger.debug(f"send key: {_key}")
                self.rmt_cmd += self.cmd_long[_index:]
            else:
                self.key_pressed_signal.emit(key)
                logger.debug(f"send key: {key}")

            if self.textCursor().atEnd():
                self.rmt_cmd += key
                self.cmd_long += key
            else:
                cur_pos = self.cur_pos_in_cmd()
                self.rmt_cmd = self.rmt_cmd[:cur_pos] + key + self.rmt_cmd[cur_pos:]
                self.cmd_long = self.cmd_long[:cur_pos] + key + self.cmd_long[cur_pos:]
            # return True
        elif e.key() == Qt.Key_Return:  # return
            key = '\n'
            cmd = self.cmd_long
            self.pressed_key('return')
            if self.rmt_cmd:
                self.key_pressed_signal.emit(key)
            else:
                for _ in range(len(cmd)):
                    self.textCursor().deletePreviousChar()
                self.key_pressed_signal.emit(self.cmd_long + key)
            self.cmd_long = ''
            self.rmt_cmd = ''
            self.cursor_pre_pos = None
            # self.new_line()
            # self.reset_cursor_pos()
            self.hide_completer(797)
            return
        elif e.key() == Qt.Key_Backspace:  # backspace
            """1. 在远端stdin存在cmd的时候，按下\\b要传过去"""
            self.pressed_key("bac")
            self.fun_pressed_signal.emit('\x08')  # 发送退格键到shell
            self.hide_completer(803)
            prefix_len = len(self.prefix.get(self.mode, ''))  # 获取当前模式要保留的前缀长度
            logger.info(
                f"Remote cmd: {repr(self.rmt_cmd)}, cmd : {repr(self.cmd_long)}") if self.rmt_cmd else ''
            # 如果前缀长度小于段落长度则可以删除字符，否则不能删除
            if prefix_len >= self.textCursor().positionInBlock() and \
                    self.textCursor().block().text().__contains__(self.prefix.get(self.mode, '')):
                logger.debug(f"mode {self.mode}, text {repr(self.textCursor().block().text())},"
                             f" prefix {repr(self.prefix.get(self.mode))}")
                logger.debug(
                    f"Prefix length:{len(self.prefix.get(self.mode))} pos:{self.textCursor().positionInBlock()}")
                return  # 拦截事件
            else:
                cur_pos = self.cur_pos_in_cmd()
                self.rmt_cmd = self.rmt_cmd[:cur_pos - 1] + self.rmt_cmd[cur_pos:]
                self.cmd_long = self.cmd_long[:cur_pos - 1] + self.cmd_long[cur_pos:]
            logger.info(f"Remote cmd: {repr(self.rmt_cmd)}") if self.rmt_cmd else ''
        elif e.key() == Qt.Key_Delete:
            logger.debug(f"Pressed delete")
            cur_pos = self.textCursor().positionInBlock() - len(self.prefix.get(self.mode))
            assert cur_pos >= 0, f"cursor position: {cur_pos}"
            self.rmt_cmd = self.rmt_cmd[:cur_pos] + self.rmt_cmd[cur_pos + 1:]
            self.cmd_long = self.rmt_cmd
            self.fun_pressed_signal.emit('\x1b[C')
            self.fun_pressed_signal.emit('\x08')
            self.pressed_key('del')
            self.hide_completer(828)
        elif e.key() == Qt.Key_A and e.modifiers() & Qt.MetaModifier:
            logger.debug(f"pressed ctrl+a")
            # pre_pos = self.textCursor().positionInBlock()
            prefix_len = len(self.prefix.get(self.mode))
            cur = self.textCursor()
            cur.setPosition(cur.block().position() + prefix_len, QTextCursor.MoveAnchor)
            self.setTextCursor(cur)
            # displacement = pre_pos - prefix_len  # 鼠标位移长度
            # [self.fun_pressed_signal.emit('\x1b[D') for _ in displacement]
            self.fun_pressed_signal.emit('\x01')
            return
        elif e.key() == Qt.Key_E and e.modifiers() & Qt.MetaModifier:
            logger.debug(f"pressed ctrl+e")
            self.fun_pressed_signal.emit('\x05')
            # return True
        elif e.key() == Qt.Key_U and e.modifiers() & Qt.MetaModifier:
            logger.debug(f"Pressed ctrl+U")
            self.fun_pressed_signal.emit('\x15')
            # 删除远端命令，删除本地当前行，并重写当前行的前缀
            self.reset_cur_line()
        elif e.key() == Qt.Key_C and e.modifiers() & Qt.MetaModifier:
            logger.debug(f"Send SIGNAL TERMINATE")
            self.fun_pressed_signal.emit('\x03')
            # return True

        self.pass_key_event_signal.emit(e)
        # return super(TextEdit, self).keyPressEvent(e)

        # TODO What's this?
        # if ctrlOrShift and len(e.text()) == 0:
        #     return
        # if len(e.text()) != 0:
        if e.text() and e.text() in string.digits + string.ascii_letters + string.punctuation + ' ':
            # self.completion_prefix = self.word_under_cursor(e.text())
            self.completion_prefix = self.cmd_long
            self._completer.setCompletionPrefix(self.completion_prefix)
            self._completer.popup().setCurrentIndex(self._completer.completionModel().index(0, 0))

            cr = self.cursorRect()
            cr.setWidth(self._completer.popup().sizeHintForColumn(0)
                        + self._completer.popup().verticalScrollBar().sizeHint().width())
            self._completer.complete(cr)
            return
        return super(TextEdit, self).keyPressEvent(e)


class CountThread(QThread):
    """
    用来给搜索词计数
    """
    count_signal = pyqtSignal(int)

    def __init__(self, keyword, text: str, word: bool = False, case: bool = False):
        super(CountThread, self).__init__()
        keyword_ = keyword if not word else f"\\b{keyword}\\b"
        if type(keyword) == QRegularExpression:
            self.k = keyword
        else:
            self.k = QRegularExpression(keyword_)
            self.k.setPatternOptions(QRegularExpression.CaseInsensitiveOption) if not case else ''
        logger.debug(self.k)
        self.t = text

    def run(self) -> None:
        # logger.debug(f"thread running")
        gm = self.k.globalMatch(self.t)
        _count = 0
        while gm.hasNext():
            _ = gm.next()
            _count += 1

        # logger.debug(f"count = {_count}, emitting")
        self.count_signal.emit(_count)


class Tracker:
    def __init__(self, *args, **kwargs):
        self._tracker = 0

    def __call__(self, direction: str = 'up'):
        if direction.lower() == 'up':
            self.tracker -= 1
        elif direction.lower() == 'down':
            self.tracker += 1
        elif direction.lower() == 'reset':
            self.tracker = 0
        return self.tracker

    @property
    def tracker(self):
        return self._tracker

    @tracker.setter
    def tracker(self, t: int):
        if t <= 0:
            self._tracker = t


if __name__ == '__main__':
    app = QApplication(sys.argv)
    app.setCursorFlashTime(0)
    t = TextEdit()
    t.show()
    sys.exit(app.exec_())
